package ace.tool.generate.code.impl.generator.liquibase;

import ace.cmp.core.constants.CoreConstant;
import ace.cmp.data.api.annotation.Column;
import ace.cmp.spring.data.jdbc.querydsl.core.annotations.LogicDelete;
import ace.cmp.spring.data.jdbc.querydsl.core.impl.QuerydslLogicNotDeleteFlagValueGetterImpl;
import ace.tool.generate.code.impl.generator.AbstractGenerator;
import ace.tool.generate.code.impl.generator.liquibase.model.LiquibaseTemplateModel;
import ace.tool.generate.code.impl.generator.liquibase.model.LiquibaseTemplateModelField;
import ace.tool.generate.code.impl.generator.liquibase.model.LiquibaseTemplateModelIndex;
import ace.tool.generate.code.impl.generator.liquibase.model.LiquibaseTemplateModelPrimaryKey;
import ace.tool.generate.code.impl.resolver.TemplateModelClassResolver;
import ace.tool.generate.code.impl.resolver.impl.TemplateModelDataJdbcClassResolverImpl;
import ace.tool.generate.code.impl.resolver.model.TemplateModel;
import ace.tool.generate.code.impl.resolver.model.TemplateModelField;
import com.google.common.base.CaseFormat;
import com.google.common.base.Function;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.sql.Time;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.annotation.Version;
import org.springframework.data.relational.core.mapping.Table;

/**
 * @author caspar
 * @date 2023/11/17 9:57 生成liquibase创建数据库的xml文件
 */
public class LiquibaseMysqlGenerator
    extends AbstractGenerator<LiquibaseMysqlGeneratorInput, LiquibaseTemplateModel> {
  private static final TemplateModelClassResolver defaultResolver =
      new TemplateModelDataJdbcClassResolverImpl();
  public static final Map<Class, Function<TemplateModelField, String>> DEFAULT_TYPE_MAP;
  private static final Function<TemplateModelField, String> resolveFieldTypeString =
      (modelField -> {
        Column columnAnn =
            AnnotatedElementUtils.findMergedAnnotation(modelField.getField(), Column.class);
        String type = Optional.ofNullable(columnAnn).map(p -> p.type()).orElse(null);
        if (StringUtils.isNotEmpty(type)) {
          return type;
        }
        int length = Optional.ofNullable(columnAnn).map(p -> p.length()).orElse(255);
        return String.format("VARCHAR(%s)", length);
      });
  private static final Function<TemplateModelField, String> resolveFieldTypeBigDecimal =
      (modelField -> {
        Column columnAnn =
            AnnotatedElementUtils.findMergedAnnotation(modelField.getField(), Column.class);
        String type = Optional.ofNullable(columnAnn).map(p -> p.type()).orElse(null);
        if (StringUtils.isNotEmpty(type)) {
          return type;
        }
        int length = Optional.ofNullable(columnAnn).map(p -> p.decimalLength()).orElse(10);
        int centLength = Optional.ofNullable(columnAnn).map(p -> p.decimalCentLength()).orElse(4);
        return String.format("DECIMAL(%s,%s)", length, centLength);
      });
  private final TemplateModelClassResolver resolver;
  private final Map<Class, Function<TemplateModelField, String>> typeMap;
  private final String defaultType;

  static {
    DEFAULT_TYPE_MAP = new HashMap<>();
    DEFAULT_TYPE_MAP.put(Integer.class, field -> "INTEGER");
    DEFAULT_TYPE_MAP.put(Long.class, field -> "BIGINT");
    DEFAULT_TYPE_MAP.put(Boolean.class, field -> "BOOLEAN");
    DEFAULT_TYPE_MAP.put(Date.class, field -> "LONG");
    DEFAULT_TYPE_MAP.put(Time.class, field -> "TIME");
    DEFAULT_TYPE_MAP.put(LocalDateTime.class, field -> "LONG");
    DEFAULT_TYPE_MAP.put(UUID.class, field -> "VARCHAR(36)");
    DEFAULT_TYPE_MAP.put(Short.class, field -> "SMALLINT");
    DEFAULT_TYPE_MAP.put(BigInteger.class, field -> "BIGINT");
    DEFAULT_TYPE_MAP.put(Timestamp.class, field -> "TIMESTAMP");
    DEFAULT_TYPE_MAP.put(String.class, resolveFieldTypeString);
    DEFAULT_TYPE_MAP.put(BigDecimal.class, resolveFieldTypeBigDecimal);
  }

  public LiquibaseMysqlGenerator() {
    this.resolver = defaultResolver;
    this.typeMap = DEFAULT_TYPE_MAP;
    this.defaultType = "TEXT";
  }

  @Override
  protected LiquibaseTemplateModel resolveModel(LiquibaseMysqlGeneratorInput input) {
    TemplateModel templateModel = resolver.resolve(input.getParams());

    String tableName = this.resolverTableName(templateModel);

    List<LiquibaseTemplateModelField> fields =
        Stream.ofNullable(templateModel.getFields())
            .flatMap(List::stream)
            .map(modelField -> this.resolveModelField(modelField))
            .collect(Collectors.toList());

    Optional<LiquibaseTemplateModelPrimaryKey> primaryKey =
        this.resolvePrimaryKey(fields, tableName);

    List<LiquibaseTemplateModelIndex> indexes = this.resolveIndexes(templateModel, tableName);

    LiquibaseTemplateModel model =
        LiquibaseTemplateModel.builder()
            .name(tableName)
            .remark(templateModel.getRemark())
            .fields(fields)
            .primaryKey(primaryKey)
            .indexes(indexes)
            .build();

    return model;
  }

  private String resolverTableName(TemplateModel templateModel) {
    String defaultName = this.toLowerUnderscore(templateModel.getName());
    Table tableAnn =
        AnnotatedElementUtils.findMergedAnnotation(templateModel.getEntityClass(), Table.class);
    String name =
        Optional.ofNullable(tableAnn)
            .map(p -> StringUtils.isBlank(p.name()) ? null : p.name())
            .orElse(defaultName);

    return this.toLowerUnderscore(name);
  }

  private List<LiquibaseTemplateModelIndex> resolveIndexes(
      TemplateModel templateModel, String tableName) {

    List<LiquibaseTemplateModelIndex> indexes =
        templateModel.getIndexes().stream()
            .map(
                index -> {
                  String indexName = index.name();

                  List<String> columns = Arrays.asList(index.columns());

                  Boolean uniqueFlag = index.unique();

                  return LiquibaseTemplateModelIndex.builder()
                      .tableName(tableName)
                      .name(indexName)
                      .columns(columns)
                      .uniqueFlag(uniqueFlag)
                      .build();
                })
            .collect(Collectors.toList());

    return indexes;
  }

  private Optional<LiquibaseTemplateModelPrimaryKey> resolvePrimaryKey(
      List<LiquibaseTemplateModelField> fields, String tableName) {
    List<LiquibaseTemplateModelField> modelFields =
        fields.stream().filter(p -> p.getIdFlag()).collect(Collectors.toList());

    if (CollectionUtils.isEmpty(modelFields)) {
      return Optional.empty();
    }

    String name = String.format("%s_id", tableName);
    String columns = modelFields.stream().map(p -> p.getName()).collect(Collectors.joining(","));

    return Optional.of(
        LiquibaseTemplateModelPrimaryKey.builder()
            .name(name)
            .tableName(tableName)
            .columns(columns)
            .build());
  }

  private LiquibaseTemplateModelField resolveModelField(TemplateModelField modelField) {
    String name = this.resolveFieldName(modelField);
    String remark = modelField.getRemark();
    String type = this.resolveFieldType(modelField);
    Boolean nullable = this.resolveFieldNullable(modelField);
    Optional<String> defaultValue = this.resolveFieldDefaultValue(modelField);
    return LiquibaseTemplateModelField.builder()
        .name(name)
        .remark(remark)
        .idFlag(modelField.getIdFlag())
        .type(type)
        .nullable(nullable)
        .defaultValue(defaultValue)
        .templateField(modelField)
        .build();
  }

  private Optional<String> resolveFieldDefaultValue(TemplateModelField modelField) {
    Column columnAnn =
        AnnotatedElementUtils.findMergedAnnotation(modelField.getField(), Column.class);
    Optional<String> result =
        Optional.ofNullable(columnAnn)
            .map(p -> StringUtils.isBlank(p.defaultValue()) ? null : p.defaultValue());
    if (result.isPresent()) {
      return result;
    }
    Optional<Version> versionAnn =
        Optional.ofNullable(
            AnnotatedElementUtils.findMergedAnnotation(modelField.getField(), Version.class));
    if (versionAnn.isPresent()) {
      return Optional.of(CoreConstant.DEFAULT_ROW_VERSION.toString());
    }
    Optional<LastModifiedDate> lastModifiedDateAnn =
        Optional.ofNullable(
            AnnotatedElementUtils.findMergedAnnotation(
                modelField.getField(), LastModifiedDate.class));
    if (lastModifiedDateAnn.isPresent()) {
      return Optional.of("(UNIX_TIMESTAMP(NOW(3))*1000)");
    }
    Optional<CreatedDate> createdDateAnn =
        Optional.ofNullable(
            AnnotatedElementUtils.findMergedAnnotation(modelField.getField(), CreatedDate.class));
    if (createdDateAnn.isPresent()) {
      return Optional.of("(UNIX_TIMESTAMP(NOW(3))*1000)");
    }
    Optional<LogicDelete> logicDeleteAnn =
        Optional.ofNullable(
            AnnotatedElementUtils.findMergedAnnotation(modelField.getField(), LogicDelete.class));
    if (logicDeleteAnn.isPresent()
        && logicDeleteAnn
            .get()
            .logicNotDeleteValueGetter()
            .equals(QuerydslLogicNotDeleteFlagValueGetterImpl.class)) {
      return Optional.of(CoreConstant.DELETE_FLAG_NOT.toString());
    }
    return Optional.empty();
  }

  private Boolean resolveFieldNullable(TemplateModelField modelField) {
    Column columnAnn =
        AnnotatedElementUtils.findMergedAnnotation(modelField.getField(), Column.class);

    Boolean nullable =
        Optional.ofNullable(columnAnn).map(p -> columnAnn.nullable()).orElse(Boolean.FALSE);

    return nullable;
  }

  private String resolveFieldName(TemplateModelField modelField) {
    Column columnAnn =
        AnnotatedElementUtils.findMergedAnnotation(modelField.getField(), Column.class);
    String defaultName = modelField.getName();
    String name =
        Optional.ofNullable(columnAnn)
            .map(p -> StringUtils.isBlank(p.name()) ? null : p.name())
            .orElse(defaultName);

    return this.toLowerUnderscore(name);
  }

  private String resolveFieldType(TemplateModelField modelField) {
    return Optional.ofNullable(this.typeMap.get(modelField.getType()))
        .map(p -> p.apply(modelField))
        .orElse(defaultType);
  }

  @Override
  protected String getFileName(
      LiquibaseMysqlGeneratorInput input, LiquibaseTemplateModel liquibaseTemplateModel) {
    return String.format("%s.xml", liquibaseTemplateModel.getName());
  }

  private String toLowerUnderscore(String value) {
    return CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, value);
  }
}
